﻿using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Web;
using System.Web.Compilation;
using System.Web.Hosting;
using System.Web.Mvc;
using MyAMS.WebServer.Services;
using MyAMS.WebServer.Models;

namespace MyAMS.WebServer.Infrastructure
{
    internal sealed class ViewEngine : IViewEngine
    {
        // 格式为 ":ViewCacheEntry:{cacheType}:{prefix}:{name}:{controllerName}:{siteID}"
        private const string cacheKeyFormat = ":ViewCacheEntry:{0}:{1}:{2}:{3}:{4}:";
        private const string cacheKeyPrefix_Master = "Master";
        private const string cacheKeyPrefix_Partial = "Partial";
        private const string cacheKeyPrefix_View = "View";
        private static readonly string[] emptyLocations = new string[0];

        private VirtualPathProvider virtualPathProvider;
        private INodeService nodeService;

        public ViewEngine(VirtualPathProvider virtualPathProvider, INodeService nodeService)
        {
            ViewLocationCache = HttpContext.Current == null || HttpContext.Current.IsDebuggingEnabled ? DefaultViewLocationCache.Null : new DefaultViewLocationCache();

            MasterLocationFormats = new[] {
                "~/Views/{2}/{1}/{0}.master",
                "~/Views/Shared/{0}.master"
            };

            PartialViewLocationFormats = ViewLocationFormats = new[] {
                "~/Views/{2}/{1}/{0}.aspx",
                "~/Views/{2}/{1}/{0}.ascx",
                "~/Views/Shared/{0}.aspx",
                "~/Views/Shared/{0}.ascx"
            };
            this.nodeService = nodeService;
            this.virtualPathProvider = virtualPathProvider;
        }

        [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")]
        public string[] MasterLocationFormats { get; set; }

        [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")]
        public string[] PartialViewLocationFormats { get; set; }

        public IViewLocationCache ViewLocationCache { get; set; }

        [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")]
        public string[] ViewLocationFormats { get; set; }

        public void ReleaseView(ControllerContext controllerContext, IView view)
        {
            IDisposable disposable = view as IDisposable;
            if (disposable != null)
            {
                disposable.Dispose();
            }
        }

        public ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
        {
            if (controllerContext == null)
            {
                throw new ArgumentNullException("controllerContext");
            }
            if (String.IsNullOrEmpty(partialViewName))
            {
                //throw new ArgumentException(MvcResources.Common_NullOrEmpty, "partialViewName");
            }

            string[] searched;
            string controllerName = controllerContext.RouteData.GetRequiredString("controller");
            string partialPath = GetPath(controllerContext, PartialViewLocationFormats, "PartialViewLocationFormats", partialViewName, controllerName, cacheKeyPrefix_Partial, useCache, out searched);

            if (String.IsNullOrEmpty(partialPath))
            {
                return new ViewEngineResult(searched);
            }

            return new ViewEngineResult(CreatePartialView(controllerContext, partialPath), this);
        }

        public ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
        {
            if (controllerContext == null)
            {
                throw new ArgumentNullException("controllerContext");
            }
            if (String.IsNullOrEmpty(viewName))
            {
                //throw new ArgumentException(MvcResources.Common_NullOrEmpty, "viewName");
            }

            string[] viewLocationsSearched;
            string[] masterLocationsSearched;

            string controllerName = controllerContext.RouteData.GetRequiredString("controller");
            string viewPath = GetPath(controllerContext, ViewLocationFormats, "ViewLocationFormats", viewName, controllerName, cacheKeyPrefix_View, useCache, out viewLocationsSearched);
            string masterPath = GetPath(controllerContext, MasterLocationFormats, "MasterLocationFormats", masterName, controllerName, cacheKeyPrefix_Master, useCache, out masterLocationsSearched);

            if (String.IsNullOrEmpty(viewPath) || (String.IsNullOrEmpty(masterPath) && !String.IsNullOrEmpty(masterName)))
            {
                return new ViewEngineResult(viewLocationsSearched.Union(masterLocationsSearched));
            }

            return new ViewEngineResult(CreateView(controllerContext, viewPath, masterPath), this);
        }

        private string CreateCacheKey(string prefix, string name, string controllerName, Guid siteID)
        {
            return String.Format(CultureInfo.InvariantCulture, cacheKeyFormat, GetType().AssemblyQualifiedName, prefix, name, controllerName, siteID.ToString());
        }

        private IView CreatePartialView(ControllerContext controllerContext, string partialPath)
        {
            return new WebFormView(partialPath, null);
        }

        private IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
        {
            return new WebFormView(viewPath, masterPath);
        }

        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are interpreted as indicating that the file does not exist.")]
        private bool FileExists(ControllerContext controllerContext, string virtualPath)
        {
            try
            {
                object viewInstance = BuildManager.CreateInstanceFromVirtualPath(virtualPath, typeof(object));

                return viewInstance != null;
            }
            catch (HttpException he)
            {
                if (he.GetHttpCode() == (int)HttpStatusCode.NotFound)
                {
                    // If BuildManager returns a 404 (Not Found) that means the file did not exist
                    return false;
                }
                else
                {
                    // All other error codes imply other errors such as compilation or parsing errors
                    throw;
                }
            }
            catch
            {
                return false;
            }
        }

        private string GetPath(ControllerContext controllerContext, string[] locations, string locationsPropertyName, string name, string controllerName, string cacheKeyPrefix, bool useCache, out string[] searchedLocations)
        {
            searchedLocations = emptyLocations;

            if (!String.IsNullOrEmpty(name))
            {
                if (locations == null || locations.Length == 0)
                {
                    //throw new InvalidOperationException(String.Format(CultureInfo.CurrentUICulture,
                    //    MvcResources.Common_PropertyCannotBeNullOrEmpty, locationsPropertyName));
                }

                Node node = nodeService.Get(controllerContext.HttpContext.Request.Url);
                if (node != null)
                {
                    bool nameRepresentsPath = IsSpecificPath(name);
                    string cacheKey = CreateCacheKey(cacheKeyPrefix, name, (nameRepresentsPath) ? String.Empty : controllerName, node.ID);

                    if (useCache)
                    {
                        string result = ViewLocationCache.GetViewLocation(controllerContext.HttpContext, cacheKey);
                        if (result != null)
                        {
                            return result;
                        }
                    }

                    return (nameRepresentsPath) ?
                        GetPathFromSpecificName(controllerContext, name, cacheKey, ref searchedLocations) :
                        GetPathFromGeneralName(controllerContext, locations, name, controllerName, node.ID, cacheKey, ref searchedLocations);
                }
            }
            return String.Empty;
        }

        private string GetPathFromGeneralName(ControllerContext controllerContext, string[] locations, string name, string controllerName, Guid siteID, string cacheKey, ref string[] searchedLocations)
        {
            string result = String.Empty;
            searchedLocations = new string[locations.Length];

            for (int i = 0; i < locations.Length; i++)
            {
                string virtualPath = String.Format(CultureInfo.InvariantCulture, locations[i], name, controllerName, siteID.ToString());

                if (FileExists(controllerContext, virtualPath))
                {
                    searchedLocations = emptyLocations;
                    result = virtualPath;
                    ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, cacheKey, result);
                    break;
                }

                searchedLocations[i] = virtualPath;
            }

            return result;
        }

        private string GetPathFromSpecificName(ControllerContext controllerContext, string name, string cacheKey, ref string[] searchedLocations)
        {
            string result = name;

            if (!FileExists(controllerContext, name))
            {
                result = String.Empty;
                searchedLocations = new[] { name };
            }

            ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, cacheKey, result);
            return result;
        }

        private static bool IsSpecificPath(string name)
        {
            char c = name[0];
            return (c == '~' || c == '/');
        }
    }
}

